package org.alerve.android.resourcebuilder.modules;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.alerve.android.resourcebuilder.FieldModuleDirector;
import org.alerve.android.resourcebuilder.HasContext;
import org.alerve.android.resourcebuilder.IModule;
import org.alerve.android.resourcebuilder.Resource;
import org.alerve.android.resourcebuilder.ResourceNameParser;
import org.alerve.android.resourcebuilder.ResourceNameParser.IParser;

import android.content.Context;
import android.view.View;
import android.view.animation.AnimationUtils;

public abstract class PopulatorModule<Type> implements IModule<Type> {

	private static class ResourceMap extends HashMap<Field, Resource> {
		private static final long serialVersionUID = -7502026659210134193L;
	}

	/**
	 * Cache used for the {@link ResourceMap} for each class processed.
	 */
	private static Map<Class<?>, ResourceMap> resourceMapCache = new HashMap<Class<?>, ResourceMap>();

	@Override
	public boolean build(Type subject) {
		Class<?> klass = subject.getClass();
		ResourceMap resourceMap = resourceMapCache.get(klass);
		if (resourceMap == null) {
			resourceMap = new ResourceMap();
			for (Field field : klass.getDeclaredFields()) {
				Resource resource = field.getAnnotation(Resource.class);
				if (resource != null) {
					resourceMap.put(field, resource);
				}
			}
		}

		try {
			for (Entry<Field, Resource> entry : resourceMap.entrySet()) {
				setValue(entry.getKey(), subject, entry.getValue());
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return true;
	}

	/**
	 * 
	 * Works out what object should be returned from the provided resource. If
	 * the resource is of type View then it will be extracted from the provided
	 * target.
	 * 
	 * @param target
	 * @param resource
	 * @return
	 */
	public Object getObject(Field field, Type target, Resource resource) {
		Context context = getContext(target);
		int id = resource.id();
		// if there is no resource id use a parser to get the name for the
		// field.
		if (id == Integer.MIN_VALUE) {
			String fieldName = field.getName();
			ResourceNameParser resourceNameParser = target.getClass()
					.getAnnotation(ResourceNameParser.class);
			IParser nameParser = ResourceNameParser.Parser
					.getParser(resourceNameParser);
			fieldName = nameParser.parse(fieldName);
			String resType = resource.type().name().toLowerCase();
			id = context.getResources().getIdentifier(fieldName, resType,
					context.getPackageName());
		}

		switch (resource.type()) {
			case Anim :
				return AnimationUtils.loadAnimation(context, id);
				// TODO support for API level 11
				// case Animator:
				// return AnimatorInflater.loadAnimator(context, id);
			case Array :
				switch (resource.arrayType()) {
					case String :
						return context.getResources().getStringArray(id);
					case Text :
						return context.getResources().getTextArray(id);
					case Integer :
						return context.getResources().getIntArray(id);
				}
			case Attr :
				return context.obtainStyledAttributes(id, resource.attr());
			case Bool :
				return context.getResources().getBoolean(id);
			case Color :
				if (!resource.colorStateList()) {
					return context.getResources().getColor(id);
				} else {
					return context.getResources().getColorStateList(id);
				}
			case Dimen :
				return context.getResources().getDimension(id);
			case Drawable :
				return context.getResources().getDrawable(id);
			case Fraction :
				return context.getResources().getFraction(id, resource.base(),
						resource.pbase());
			case Id :
				return findViewById(target, id);
			case Integer :
				return context.getResources().getInteger(id);
			case Interpolator :
				return AnimationUtils.loadInterpolator(context, id);
			case Layout :
				return context.getResources().getLayout(id);
				// TODO menus not supported
				// case Menu:
				// TODO support for API level 11
				// case MinMap
			case Plurals :
				if (resource.arguments().length == 0) {
					return context.getResources().getQuantityText(id,
							resource.quatity());
				} else {
					Object[] arguments = (Object[]) resource.arguments();
					return context.getResources().getQuantityString(id,
							resource.quatity(), arguments);
				}
			case Raw :
				switch (resource.rawType()) {
					case InputStream :
						return context.getResources().openRawResource(id);
					case Movie :
						return context.getResources().getMovie(id);
					case AssetFileDescriptor :
						return context.getResources().openRawResourceFd(id);
					default :
						break;
				}
			case String :
				if (resource.arguments().length == 0) {
					return context.getText(id);
				} else {
					Object[] arguments = (Object[]) resource.arguments();
					return context.getString(id, arguments);
				}
			case Xml :
				return context.getResources().getXml(id);
			default :
				// TODO inject further options for api level 11
				throw new RuntimeException("Type of resource not supported.");
		}
	}

	/**
	 * 
	 * Internal method used by populate(Type subject) to find the correct view
	 * in the specific type being populated.
	 * 
	 * @param parent
	 * @param id
	 * @return
	 */
	protected abstract View findViewById(Type parent, int id);

	private static FieldModuleDirector director;
	
	private static FieldModuleDirector getDirector() {
		if (director == null) {
			director = FieldModuleDirector.getInstance();
		}
		return director;
	}

	/**
	 * 
	 * Assigns an object indicated by the {@link Resource} argument to the
	 * provided field in target.
	 * 
	 * @param field
	 * @param target
	 * @param resource
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 */
	private void setValue(Field field, Type target, Resource resource)
			throws IllegalArgumentException, IllegalAccessException {
		Object value = getObject(field, target, resource);
		if (getDirector().hasModuleFor(value)) {
			getDirector().build(getContext(value), value, field);
		}
		if (value != null) {
			if (!field.isAccessible()) {
				field.setAccessible(true);
			}
			field.set(target, value);
		} else {
			throw new RuntimeException("There was a problem populating "
					+ field.getName() + " in " + target.getClass().getName());
		}
	}

	/**
	 * 
	 * Retrieves the Context from the provided object or throws an exception if
	 * it doesn't have one.
	 * 
	 * @param object
	 * @return
	 */
	private static Context getContext(Object object) {
		Context context = null;
		if (object instanceof Context) {
			context = (Context) object;
		} else if (object instanceof View) {
			context = ((View) object).getContext();
		} else if (object instanceof HasContext) {
			context = ((HasContext) object).getContext();
		}
		if (context == null) {
			throw new RuntimeException("The object should proide with "
					+ "a Context, try extending HasContext.");
		}
		return context;
	}

}
